/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.datasource;

import javax.annotation.Nullable;

import java.util.List;
import java.util.concurrent.Executor;

import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.internal.Lists;
import com.facebook.common.internal.Supplier;

import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.mockito.verification.VerificationMode;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class DataSourceTestUtils {

    public static final boolean CLOSED = true;
    public static final boolean NOT_CLOSED = false;
    public static final boolean FINISHED = true;
    public static final boolean NOT_FINISHED = false;
    public static final boolean WITH_RESULT = true;
    public static final boolean WITHOUT_RESULT = false;
    public static final boolean FAILED = true;
    public static final boolean NOT_FAILED = false;
    public static final boolean LAST = true;
    public static final boolean INTERMEDIATE = false;
    public static final int NO_INTERACTIONS = 0;
    public static final int ON_NEW_RESULT = 1;
    public static final int ON_FAILURE = 2;
    public static final int ON_CANCELLATION = 3;

    public static VerificationMode optional() {
        return atLeast(0);
    }

    public static void setState(
            DataSource<Object> dataSource,
            boolean isClosed,
            boolean isFinished,
            boolean hasResult,
            Object value,
            boolean hasFailed,
            Throwable failureCause) {
        when(dataSource.isClosed()).thenReturn(isClosed);
        when(dataSource.isFinished()).thenReturn(isFinished);
        when(dataSource.hasResult()).thenReturn(hasResult);
        when(dataSource.getResult()).thenReturn(value);
        when(dataSource.hasFailed()).thenReturn(hasFailed);
        when(dataSource.getFailureCause()).thenReturn(failureCause);
    }

    public static <T> void verifyState(
            DataSource<T> dataSource,
            boolean isClosed,
            boolean isFinished,
            boolean hasResult,
            T result,
            boolean hasFailed,
            Throwable failureCause) {
        assertEquals("isClosed", isClosed, dataSource.isClosed());
        assertEquals("isFinished", isFinished, dataSource.isFinished());
        assertEquals("hasResult", hasResult, dataSource.hasResult());
        assertSame("getResult", result, dataSource.getResult());
        assertEquals("hasFailed", hasFailed, dataSource.hasFailed());
        assertSame("failureCause", failureCause, dataSource.getFailureCause());
    }

    public static class AbstractDataSourceSupplier {

        protected DataSource<Object> mSrc1;
        protected DataSource<Object> mSrc2;
        protected DataSource<Object> mSrc3;
        protected Supplier<DataSource<Object>> mDataSourceSupplier1;
        protected Supplier<DataSource<Object>> mDataSourceSupplier2;
        protected Supplier<DataSource<Object>> mDataSourceSupplier3;
        protected DataSubscriber<Object> mDataSubscriber;
        protected Executor mExecutor;
        protected InOrder mInOrder;
        protected List<Supplier<DataSource<Object>>> mSuppliers;
        protected Supplier<DataSource<Object>> mDataSourceSupplier;

        public void setUp() {
            mSrc1 = mock(DataSource.class);
            mSrc2 = mock(DataSource.class);
            mSrc3 = mock(DataSource.class);
            mDataSourceSupplier1 = mock(Supplier.class);
            mDataSourceSupplier2 = mock(Supplier.class);
            mDataSourceSupplier3 = mock(Supplier.class);
            when(mDataSourceSupplier1.get()).thenReturn(mSrc1);
            when(mDataSourceSupplier2.get()).thenReturn(mSrc2);
            when(mDataSourceSupplier3.get()).thenReturn(mSrc3);
            mDataSubscriber = mock(DataSubscriber.class);
            mExecutor = CallerThreadExecutor.getInstance();
            mInOrder = inOrder(
                    mSrc1,
                    mSrc2,
                    mSrc3,
                    mDataSourceSupplier1,
                    mDataSourceSupplier2,
                    mDataSourceSupplier3,
                    mDataSubscriber);
            mSuppliers = Lists.newArrayList(
                    mDataSourceSupplier1,
                    mDataSourceSupplier2,
                    mDataSourceSupplier3);
        }

        protected void verifyNoMoreInteractionsAll() {
            verifyOptionals(mSrc1);
            verifyOptionals(mSrc2);
            verifyOptionals(mSrc3);
            mInOrder.verifyNoMoreInteractions();
            verifyNoMoreInteractions(
                    mSrc1,
                    mSrc2,
                    mSrc3,
                    mDataSourceSupplier1,
                    mDataSourceSupplier2,
                    mDataSourceSupplier3,
                    mDataSubscriber);
        }

        protected void verifyOptionals(DataSource<Object> underlyingDataSource) {
            mInOrder.verify(underlyingDataSource, optional()).isFinished();
            mInOrder.verify(underlyingDataSource, optional()).hasResult();
            mInOrder.verify(underlyingDataSource, optional()).hasFailed();
            verify(underlyingDataSource, optional()).isFinished();
            verify(underlyingDataSource, optional()).hasResult();
            verify(underlyingDataSource, optional()).hasFailed();
        }

        /**
         * Verifies that our mDataSourceSupplier got underlying data source and subscribed to it.
         * Subscriber is returned.
         */
        protected DataSubscriber<Object> verifyGetAndSubscribe(
                Supplier<DataSource<Object>> dataSourceSupplier,
                DataSource<Object> underlyingDataSource,
                boolean expectMoreInteractions) {
            mInOrder.verify(dataSourceSupplier).get();
            ArgumentCaptor<DataSubscriber> captor = ArgumentCaptor.forClass(DataSubscriber.class);
            mInOrder.verify(underlyingDataSource).subscribe(captor.capture(), any(Executor.class));
            if (!expectMoreInteractions) {
                verifyNoMoreInteractionsAll();
            }
            return captor.getValue();
        }

        protected DataSubscriber<Object> verifyGetAndSubscribe(
                Supplier<DataSource<Object>> dataSourceSupplier,
                DataSource<Object> underlyingDataSource) {
            return verifyGetAndSubscribe(dataSourceSupplier, underlyingDataSource, false);
        }

        protected DataSubscriber<Object> verifyGetAndSubscribeM(
                Supplier<DataSource<Object>> dataSourceSupplier,
                DataSource<Object> underlyingDataSource) {
            return verifyGetAndSubscribe(dataSourceSupplier, underlyingDataSource, true);
        }

        /**
         * Verifies that data source provided by our mDataSourceSupplier notified mDataSubscriber.
         */
        protected void verifySubscriber(
                DataSource<Object> dataSource,
                DataSource<Object> underlyingDataSource,
                int expected) {
            switch (expected) {
                case NO_INTERACTIONS:
                    verifyNoMoreInteractionsAll();
                    break;
                case ON_NEW_RESULT:
                    mInOrder.verify(mDataSubscriber).onNewResult(dataSource);
                    verifyNoMoreInteractionsAll();
                    break;
                case ON_FAILURE:
                    mInOrder.verify(underlyingDataSource).getFailureCause();
                    mInOrder.verify(mDataSubscriber).onFailure(dataSource);
                    verifyNoMoreInteractionsAll();
                    break;
                case ON_CANCELLATION:
                    verify(mDataSubscriber).onCancellation(dataSource);
                    verifyNoMoreInteractionsAll();
                    break;
            }
        }

        /**
         * Verifies the state of the data source provided by our mDataSourceSupplier.
         */
        protected void verifyState(
                DataSource<Object> dataSource,
                @Nullable DataSource<Object> dataSourceWithResult,
                boolean isClosed,
                boolean isFinished,
                boolean hasResult,
                Object result,
                boolean hasFailed,
                Throwable failureCause) {
            DataSourceTestUtils.verifyState(
                    dataSource, isClosed, isFinished, hasResult, result, hasFailed, failureCause);
            // DataSourceTestUtils.verifyState will call dataSource.getResult() which should forward to
            // underlyingDataSource.getResult()
            if (dataSourceWithResult != null) {
                mInOrder.verify(dataSourceWithResult).getResult();
            }
            verifyNoMoreInteractionsAll();
        }

        /**
         * Verifies that the underlying data sources get closed when data source provided by
         * our mDataSourceSupplier gets closed.
         */
        protected void testClose(
                DataSource<Object> dataSource,
                DataSource<Object>... underlyingDataSources) {
            dataSource.close();
            if (underlyingDataSources != null) {
                for (DataSource<Object> underlyingDataSource : underlyingDataSources) {
                    mInOrder.verify(underlyingDataSource, atLeastOnce()).close();
                }
            }
        }

        /**
         * Gets data source from our mDataSourceSupplier and subscribes mDataSubscriber to it.
         * Obtained data source is returned.
         */
        protected DataSource<Object> getAndSubscribe() {
            DataSource<Object> dataSource = mDataSourceSupplier.get();
            dataSource.subscribe(mDataSubscriber, mExecutor);
            return dataSource;
        }

        /**
         * Respond to subscriber with given data source and response.
         */
        protected static <T> void respond(
                DataSubscriber<T> subscriber,
                DataSource<T> dataSource,
                int response) {
            switch (response) {
                case NO_INTERACTIONS:
                    break;
                case ON_NEW_RESULT:
                    subscriber.onNewResult(dataSource);
                    break;
                case ON_FAILURE:
                    subscriber.onFailure(dataSource);
                    break;
                case ON_CANCELLATION:
                    subscriber.onCancellation(dataSource);
                    break;
            }
        }

        /**
         * Schedule response on subscribe.
         */
        protected static <T> void respondOnSubscribe(
                final DataSource<T> dataSource,
                final int response) {
            doAnswer(
                    new Answer() {
                        @Override
                        public Object answer(InvocationOnMock invocation) throws Throwable {
                            DataSubscriber<T> subscriber = (DataSubscriber<T>) invocation.getArguments()[0];
                            respond(subscriber, dataSource, response);
                            return subscriber;
                        }
                    }).when(dataSource).subscribe(any(DataSubscriber.class), any(Executor.class));
        }
    }
}
